﻿using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace Reflective
{
    /// <summary>
    /// Extension methods for <see cref="TypeBuilder"/>.
    /// </summary>
    public static class TypeBuilderExtensions
    {
        /// <summary>
        /// Adds a <see cref="DebuggerDisplayAttribute"/> attribute to
        /// the specified type.
        /// </summary>
        /// <param name="type">
        /// The <see cref="TypeBuilder"/> for the type that is being
        /// modified.
        /// </param>
        /// <param name="expression">
        /// The string expression to use for the display.
        /// </param>
        /// <returns>
        /// The <see cref="TypeBuilder"/> passed in, to promote a
        /// fluent interface.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="type"/> is <c>null</c>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="expression"/> is <c>null</c> or empty.</para>
        /// </exception>
        public static TypeBuilder DebuggerDisplay(this TypeBuilder type, string expression)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (StringEx.IsNullOrWhiteSpace(expression))
                throw new ArgumentNullException("expression");

            var attr = new CustomAttributeBuilder(
                typeof(DebuggerDisplayAttribute).GetConstructor(new[] { typeof(string) }),
                new object[] { expression });
            type.SetCustomAttribute(attr);
            return type;
        }

        /// <summary>
        /// Emits the necessary IL to implement, and call, the base constructor of a class,
        /// and then returns the <see cref="ConstructorBuilder"/> object,
        /// ready to emit custom code for the constructor method body.
        /// </summary>
        /// <param name="type">
        /// The <see cref="TypeBuilder"/> to implement the constructor for.
        /// </param>
        /// <param name="constructor">
        /// The <see cref="ConstructorInfo"/> object from the base type,
        /// corresponding to the constructor to implement.
        /// </param>
        /// <returns>
        /// The <see cref="ILGenerator"/> object for the implemented
        /// constructor, ready to add custom code. At the very least,
        /// a <see cref="OpCodes.Ret"/> instruction must be added.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="type"/> is <c>null</c>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="constructor"/> is <c>null</c>.</para>
        /// </exception>
        public static ConstructorBuilder ImplementBaseConstructor(this TypeBuilder type, ConstructorInfo constructor)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (constructor == null)
                throw new ArgumentNullException("constructor");

            ParameterInfo[] ctorParameters = constructor.GetParameters();
            Type[] ctorParameterTypes =
                (from parm in ctorParameters
                 select parm.ParameterType).ToArray();
            ConstructorBuilder implementation = type.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis, ctorParameterTypes);
            for (int index = 1; index <= ctorParameters.Length; index++)
                implementation.DefineParameter(index, ctorParameters[index - 1].Attributes, ctorParameters[index - 1].Name);

            implementation.GetILGenerator()
                .ldargs(Enumerable.Range(0, constructor.GetParameters().Length + 1))
                .call(constructor)
                .ret();
            return implementation;
        }

        /// <summary>
        /// Creates a private backing field for the specified property.
        /// </summary>
        /// <param name="type">
        /// The type that will contain the <paramref name="property"/>.
        /// </param>
        /// <param name="property">
        /// The property to declare a backing field for.
        /// </param>
        /// <returns>
        /// The <see cref="FieldBuilder"/> object for the created field.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="type"/> is <c>null</c>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="property"/> is <c>null</c>.</para>
        /// </exception>
        public static FieldBuilder CreateBackingField(this TypeBuilder type, PropertyInfo property)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (property == null)
                throw new ArgumentNullException("property");

            string backingFieldName = String.Format(CultureInfo.InvariantCulture, "{0}_<0x{1:x8}>", property.Name, property.MetadataToken);
            return type
                .DefineField(backingFieldName, property.PropertyType, FieldAttributes.Private)
                .DebuggerBrowsable(DebuggerBrowsableState.Never);
        }

        /// <summary>
        /// Adds the necessary definitions to the type in order to implement
        /// an interface method, and returns the <see cref="ILGenerator"/>
        /// object, ready to add code to the method.
        /// </summary>
        /// <param name="type">
        /// The <see cref="TypeBuilder"/> to implement the method for.
        /// </param>
        /// <param name="method">
        /// The interface method <see cref="MethodInfo"/> to implement.
        /// </param>
        /// <returns>
        /// The <see cref="MethodBuilder"/> of the method that was added.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="type"/> is <c>null</c>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="method"/> is <c>null</c>.</para>
        /// </exception>
        public static MethodBuilder ImplementInterfaceMethod(this TypeBuilder type, MethodInfo method)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (method == null)
                throw new ArgumentNullException("method");

            ParameterInfo[] parameters = method.GetParameters();
            Type[] methodParameterTypes =
                (from parameter in parameters
                 select parameter.ParameterType).ToArray();

            MethodBuilder implementation = type.DefineMethod(method.Name,
                MethodAttributes.Private | MethodAttributes.Virtual,
                method.ReturnType, methodParameterTypes);
            if (method.IsGenericMethod)
                implementation.DefineGenericParameters((from a in method.GetGenericArguments()
                                                        select a.Name).ToArray());

            for (int index = 0; index < parameters.Length; index++)
                implementation.DefineParameter(index + 1, parameters[index].Attributes, parameters[index].Name);

            type.DefineMethodOverride(implementation, method);
            return implementation;
        }

        /// <summary>
        /// Adds an override for the given method, and returns the
        /// <see cref="MethodBuilder"/> for the added method.
        /// </summary>
        /// <param name="type">
        /// The <see cref="TypeBuilder"/> to implement the method for.
        /// </param>
        /// <param name="methodName">
        /// The name of the method to override.
        /// </param>
        /// <param name="parameterTypes">
        /// The <see cref="Type"/> objects for the parameter types to the
        /// method.
        /// </param>
        /// <returns>
        /// The <see cref="MethodBuilder"/> for the method that was added.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="type"/> is <c>null</c>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="methodName"/> is <c>null</c> or empty.</para>
        /// </exception>
        public static MethodBuilder OverrideMethod(this TypeBuilder type, string methodName, params Type[] parameterTypes)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (StringEx.IsNullOrWhiteSpace(methodName))
                throw new ArgumentNullException("methodName");

            parameterTypes = parameterTypes ?? new Type[0];

            Type baseType = type.BaseType;
            Debug.Assert(baseType != null, "baseType cannot be null here");

            MethodInfo method = baseType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null);
            return OverrideMethod(type, method);
        }

        /// <summary>
        /// Adds an override for the given method, and returns the
        /// <see cref="MethodBuilder"/> for the added method.
        /// </summary>
        /// <param name="type">
        /// The <see cref="TypeBuilder"/> to implement the method for.
        /// </param>
        /// <param name="method">
        /// The <see cref="MethodInfo"/> object of the method to override.
        /// </param>
        /// <returns>
        /// The <see cref="MethodBuilder"/> for the method that was added.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="type"/> is <c>null</c>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="method"/> is <c>null</c>.</para>
        /// </exception>
        public static MethodBuilder OverrideMethod(this TypeBuilder type, MethodInfo method)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (method == null)
                throw new ArgumentNullException("method");

            MethodBuilder implementation = type.DefineMethod(
                method.Name,
                method.Attributes,
                method.CallingConvention,
                method.ReturnType,
                (from parameter in method.GetParameters()
                 select parameter.ParameterType).ToArray());
            if (method.IsGenericMethod)
            {
                implementation.DefineGenericParameters(
                    (from index in Enumerable.Range(1, method.GetGenericArguments().Length)
                     select index.ToString(CultureInfo.InvariantCulture)).ToArray());
            }

            type.DefineMethodOverride(implementation, method);
            return implementation;
        }
    }
}